package bancosys.tec.persist.dao;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import jmine.tec.annotations.Unmodifiable;
import jmine.tec.persist.PersistMessages;
import jmine.tec.persist.chinese.ChineseWallFilterRegistry;
import jmine.tec.persist.naturalkey.NaturalKeyHandlerFactory;

import org.springframework.transaction.PlatformTransactionManager;

import bancosys.tec.component.ControllerLocator;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.hibernate.page.PagingService;
import bancosys.tec.persist.span.SpanAwareBean;
import bancosys.tec.persist.span.fat.FatSpanAwareBean;
import bancosys.tec.persist.span.fat.SimpleFatSpanAwareDAO;
import bancosys.tec.persist.span.fat.SimpleSpanAwareDAO;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Factory de DAOs.
 */
public class BaseDAOFactory {

    private ControllerLocator controllerLocator;

    private PlatformTransactionManager transactionManager;

    private ChineseWallFilterRegistry chineseWallFilterRegistry;

    private PagingService pagingService;

    /**
     * Maps (BoClass, DAOClass), acts as cache to avoid ReflectionUtils.findClass
     */
    private Map<Class<?>, Class<?>> daoClassCacheByBoClass = new ConcurrentHashMap<Class<?>, Class<?>>();

    /**
     * Maps (DAOClass, BaseDAO), acts as cache to avoid creating DAO. Only @Unmodifiable DAOs can use this cache
     */
    private Map<Class<?>, BaseDAO<?>> daoInstanceCacheByDAOClass = new ConcurrentHashMap<Class<?>, BaseDAO<?>>();

    /**
     * Factory de handlers de natural key.
     */
    private NaturalKeyHandlerFactory naturalKeyHandlerFactory;

    /**
     * Nome do package onde ficam os beans.
     */
    public static final String BEAN_PACKAGE = "domain";

    /**
     * Nome do package onde ficam os dados.
     */
    public static final String DAO_PACKAGE = "dao";

    /**
     * Devolve um baseDAO generico para o tipo passado.
     * 
     * @param <B> o tipo
     * @param type a classe
     * @return {@link BaseDAO} generico para o tipo B
     */
    public <B extends Persistable> BaseDAO<B> getGenericDAO(Class<? extends B> type) {
        return this.getDAO(type);
    }

    /**
     * Recupera o DAO a partir de sua classe. Este método assume o padrão que o DAO está no pacote .dao a partir do pacote da classe, e que
     * possui 'DAO' no fim do nome. Exemplo: Classe: bancosys.tec.persist.domain.base.FirstDummy DAO:
     * bancosys.tec.persist.dao.base.FirstDummyDAO
     * 
     * @param boClass a classe de domínio para qual se deseja recuperar o dao
     * @param <DAO> classe do DAO
     * @param <Bo> classe do BO
     * @return o dao
     * @deprecated usar ou {@link #getDAOByClass(Class)} quando a classe cliente souber exatamente o tipo do dao ou
     * {@link #getGenericDAO(Class)} para pegar um dao generico
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public <Bo extends Persistable, DAO extends BaseDAO<Bo>> DAO getDAO(Class<? extends Bo> boClass) {
        DAO dao = null;
        PersistenceEnabledController controller = (PersistenceEnabledController) this.controllerLocator.findControllerForClass(boClass);
        Class<DAO> daoClass = (Class<DAO>) this.daoClassCacheByBoClass.get(boClass);
        if (daoClass != null) {
            if (this.isSimpleDAOClass(daoClass)) {
                return (DAO) this.newSimpleDAOFor(boClass, controller);
            } else {
                return this.createDAO(daoClass, controller);
            }
        }
        String boClassName = this.getRealClass(boClass).getName();
        int index = boClassName.indexOf("." + BaseDAOFactory.BEAN_PACKAGE + ".");
        if (index == -1) {
            throw new PersistenceException(PersistMessages.BEAN_NOT_ON_STANDARD_PACKAGE.create(boClass.getName()));
        }
        String classNamePrefix = boClassName.substring(0, index);
        String classNameSuffix = boClassName.substring(index + ("." + BaseDAOFactory.BEAN_PACKAGE + ".").length());
        String daoClassName = classNamePrefix + "." + BaseDAOFactory.DAO_PACKAGE + "." + classNameSuffix + "DAO";

        try {
            daoClass = (Class<DAO>) ReflectionUtils.findClass(daoClassName, this.getClass().getClassLoader());
            dao = this.createDAO(daoClass, controller);
        } catch (ClassNotFoundException e) {
            dao = (DAO) this.newSimpleDAOFor(boClass, controller);
            daoClass = (Class<DAO>) dao.getClass();
        }
        this.daoClassCacheByBoClass.put(boClass, daoClass);
        return dao;
    }

    /**
     * @param <T> the type
     * @param controller the controller
     * @param type the type
     * @return {@link BaseDAO}
     */
    @SuppressWarnings("unchecked")
    private <T extends Persistable> BaseDAO<T> newSimpleDAOFor(Class<? extends T> type, PersistenceEnabledController controller) {
        final BaseDAO<T> simpleDAO;
        if (SpanAwareBean.class.isAssignableFrom(type)) {
            simpleDAO = new SimpleSpanAwareDAO(type);
        } else if (FatSpanAwareBean.class.isAssignableFrom(type)) {
            simpleDAO = new SimpleFatSpanAwareDAO(type);
        } else {
            simpleDAO = new SimpleDAO<T>(type);
        }
        this.injectDependencies(controller, simpleDAO);
        return simpleDAO;
    }

    /**
     * Retorna true caso a class seja de uma das implementações de DAO genéricas (retornadas por newSimpleDAOFor)
     * 
     * @param daoClass daoClass
     * @return boolean
     */
    private boolean isSimpleDAOClass(Class<?> daoClass) {
        return daoClass.equals(SimpleDAO.class) || daoClass.equals(SimpleFatSpanAwareDAO.class)
                || daoClass.equals(SimpleSpanAwareDAO.class);
    }

    /**
     * Recupera o DAO a partir da classe de BO e de sua classe. Este método tipicamente é utilizado para extensões de DAOs. Para o caso
     * geral de DAO de BO deve ser utilizado o método getDAO.
     * 
     * @param daoClass a classe do DAO.
     * @param <DAO> classe do DAO
     * @param <Bo> classe do BO
     * @return o dao
     */

    public <Bo extends Persistable, DAO extends BaseDAO<Bo>> DAO getDAOByClass(Class<DAO> daoClass) {
        PersistenceEnabledController controller = (PersistenceEnabledController) this.controllerLocator.findControllerForClass(daoClass);
        if (daoClass.isAnnotationPresent(Unmodifiable.class)) {
            @SuppressWarnings("unchecked")
            DAO dao = (DAO) this.daoInstanceCacheByDAOClass.get(daoClass);
            if (dao == null) {
                dao = this.createDAO(daoClass, controller);
                this.daoInstanceCacheByDAOClass.put(daoClass, dao);
            }
            return dao;
        } else {
            return this.createDAO(daoClass, controller);
        }
    }

    /**
     * Cria o DAO e injeta as dependencias.
     * 
     * @param <DAO> Classe do dao
     * @param <Bo> classe do bo
     * @param daoClass Classe do dao
     * @param controller controller
     * @return dao.
     */
    private <Bo extends Persistable, DAO extends BaseDAO<Bo>> DAO createDAO(Class<DAO> daoClass, PersistenceEnabledController controller) {
        try {
            DAO dao = daoClass.newInstance();
            this.injectDependencies(controller, dao);
            return dao;
        } catch (SecurityException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_DAO.create(daoClass.getName()), e);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_DAO.create(daoClass.getName()), e);
        } catch (InstantiationException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_DAO.create(daoClass.getName()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_DAO.create(daoClass.getName()), e);
        }
    }

    /**
     * @param controller controller
     * @param dao dao
     */
    protected void injectDependencies(PersistenceEnabledController controller, BaseDAO<?> dao) {
        dao.setController(controller);
        dao.setChineseWallFilterRegistry(this.chineseWallFilterRegistry);
        dao.setTransactionManager(this.transactionManager);
        dao.setPagingService(this.pagingService);
        dao.setNaturalKeyHandlerFactory(this.naturalKeyHandlerFactory);
    }

    /**
     * Dada uma classe com proxy devolve a classe "original"
     * 
     * @param clazz classe
     * @return classe
     */
    private Class<?> getRealClass(Class<?> clazz) {
        if (clazz.getName().indexOf("$$") > 0) {
            return clazz.getSuperclass();
        } else {
            return clazz;
        }
    }

    /**
     * Define o transaction manager.
     * 
     * @param transactionManager transaction manager.
     */
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    /**
     * Define o controller locator
     * 
     * @param controllerLocator controller locator.
     */
    public void setControllerLocator(ControllerLocator controllerLocator) {
        this.controllerLocator = controllerLocator;
    }

    /**
     * @param chineseWallFilterRegistry the chineseWallFilterRegistry to set
     */
    public void setChineseWallFilterRegistry(ChineseWallFilterRegistry chineseWallFilterRegistry) {
        this.chineseWallFilterRegistry = chineseWallFilterRegistry;
    }

    /**
     * @param pagingService the pagingService to set
     */
    public void setPagingService(PagingService pagingService) {
        this.pagingService = pagingService;
    }

    /**
     * @param naturalKeyHandlerFactory the naturalKeyHandlerFactory to set
     */
    public void setNaturalKeyHandlerFactory(NaturalKeyHandlerFactory naturalKeyHandlerFactory) {
        this.naturalKeyHandlerFactory = naturalKeyHandlerFactory;
    }
}
